/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once

#include <baja/common.hpp>
#include <baja/error.hpp>
#include <baja/assert.hpp>
#include <baja/math_fwd.hpp>

namespace baja { namespace math {

// point2d class, provides 2D point functionality
struct point2d
{
    float32 x;
    float32 y;

    point2d() : x(0.f), y(0.f) {}
    point2d(const float32 x_, const float32 y_) : x(x_), y(y_) {}

    point2d(const point2d& other) : x(other.x), y(other.y) {}
    point2d(const vector2d& vector);

    bool operator==(const point2d& other) const;
    bool operator!=(const point2d& other) const;
    point2d& operator=(const point2d& other);

    void set(float32 x, float32 y);

    void offsetBy(const vector2d& vector);
    void offsetBy(const vector2d& vector, point2d* result) const;
    void vectorTo(const point2d& point, vector2d* result) const;

    void scale(float32 scale);
    void scale(float32 scale, point2d* result);

    void lerpTo(const point2d& point, float32 t, point2d* result) const;
};

// vector2d class, provides 2D vector functionality
struct vector2d
{
    float32 x;
    float32 y;

    vector2d() : x(0.f), y(0.f) {}
    vector2d(const float32 x_, const float32 y_) : x(x_), y(y_) {}
    vector2d(const vector2d& other) : x(other.x), y(other.y) {}
    vector2d(const point2d& point);

    bool operator==(const vector2d& other) const;
    bool operator!=(const vector2d& other) const;
    vector2d& operator=(const vector2d& other);

    vector2d&  operator+=(const vector2d& that);
    vector2d&  operator-=(const vector2d& that);

    void set(float32 x, float32 y);
    void add(const vector2d& that, vector2d* result) const;
    void subtract(const vector2d& that, vector2d* result) const;
    void scale(float32 scale);
    void scale(float32 scale, vector2d* result) const;
	float32 angleBetween(const vector2d other) const;

    float32 lengthSquared() const;
    float32 length() const;
    float32 dot(const vector2d& that) const;
    void cross(vector2d* result) const;     
    void lerpTo(const vector2d& v, float32 t, vector2d* result) const;
    void normalize();
    void normalize(vector2d* result) const;
};

// 2d rectangle
struct rectangle
{
    float32 left;
    float32 top;
    float32 right;
    float32 bottom;
    
    rectangle() : left(0.f), top(0.f), right(0.f), bottom(0.f) {}
    rectangle(float32 l, float32 t, float32 r, float32 b);

    void set(float32 l, float32 t, float32 r, float32 b);
    void translate(const vector2d& translationVector);
    bool intersects(const rectangle& other) const;
    template <typename T>
    void getSize(T* width, T* height) const;
    template <typename T>
    void getCenter(T* x, T* y) const;
    void forceIntoBounds(const rectangle& bounds);
};

// point3d class - provides 3D point functionality
struct point3d
{
    float32 x;
    float32 y;
    float32 z;

    point3d() : x(0.f), y(0.f), z(0.f) {}
    point3d(const float32 x_, const float32 y_, const float32 z_) : x(x_), y(y_), z(z_) {}
    point3d(const point3d& point) : x(point.x), y(point.y), z(point.z) {}
    point3d(const vector3d& vector);

    bool operator==(const point3d& that) const;
    bool operator!=(const point3d& that) const;
    point3d& operator=(const point3d& that);

    void set(const float32 x, const float32 y, const float32 z);
    void offsetBy(const vector3d& v);
    void offsetBy(const vector3d& v, point3d* result) const;
    void vectorTo(const point3d& p, vector3d* result) const;

    void scale(const float32 scale);
    void scale(const float32 scale, point3d* result);

    void lerpTo(const point3d& p, const float32 t, point3d* result) const;

    float32 distanceToLine(const line3d& line) const;
};

// vector3d class, provides 3D vector functionality
struct vector3d
{
    float32 x;
    float32 y;
    float32 z;

    vector3d() : x(0.f), y(0.f), z(0.f) {} 
    vector3d(const float32 x_, const float32 y_, const float32 z_) : x(x_), y(y_), z(z_) {}
    vector3d(const vector3d& vector) : x(vector.x), y(vector.y), z(vector.z) {}
    vector3d(const point3d& point);

    bool operator==(const vector3d& that) const;
    bool operator!=(const vector3d& that) const;

    vector3d& operator=(const vector3d& that);
    vector3d& operator+=(const vector3d& that);
    vector3d& operator-=(const vector3d& that);

    void set(const float32 x, const float32 y, const float32 z);
    void add(const vector3d& that, vector3d* result) const;
    void subtract(const vector3d& that, vector3d* result) const;
    void scale(const float32 scale);
    void scale(const float32 scale, vector3d* result) const;
    
    float32 angleBetween(const vector3d other) const;
    float32 angleBetweenNormalized(const vector3d other) const;

    float32 lengthSquared() const;
    float32 length() const;
    float32 dot(const vector3d& that) const;
    void cross(const vector3d& that, vector3d* result) const;
    void lerpTo(const vector3d& v, const float32 t, vector3d* result) const;
    void normalize();
    void normalize(vector3d* result) const;
};

// provides basic line functionality
struct line3d
{
    point3d point;
    vector3d direction;

    line3d() {}
    line3d(const point3d& point, const vector3d& direction);
    line3d(const point3d& start, const point3d& end);
    void set(const point3d& point, const vector3d& direction);
    void set(const point3d& start, const point3d& end);
    void getPointAt(const float32 t, point3d* result) const;
    bool intersects(const plane3d& plane, float32* intersectionT) const;

     bool intersectsAABB(
        const vector3d& boxMin,
        const vector3d& boxMax,
        float32* outIntersectionT
        ) const;

     bool intersectsTriangle(
        const point3d& v0,
        const point3d& v1,
        const point3d& v2,
        float32* outIntersectionT
        ) const;

     bool intersectsSphere(
        const point3d& spherePosition,
        float32 sphereRadius,
        float32* outIntersectionT
        ) const;

    bool shortestLineBetween(const line3d line, float32* outT1, float32* outT2);
};

// provides basic plane functionality
struct plane3d
{
    point3d point;
    vector3d normal;

    plane3d() {}
    plane3d(const point3d& point, const vector3d& normal);
    plane3d(const point4d& abcd);
    void set(const point3d& point, const vector3d& normal);
    
    void project(const vector3d& vec, vector3d* outVec) const;
    void project(const point3d& pos, point3d* outPoint) const;

    bool isPointAbovePlane(const point3d& p);
    bool isPointOnOrAbovePlane(const point3d& p);    
    bool isPointBelowPlane(const point3d& p);
    bool isPointOnOrBelowPlane(const point3d& p);
    bool isPointOnPlane(const point3d& p);
};

// point4d class, provides 4D point functionality
struct point4d
{
    float32 x;
    float32 y;
    float32 z;
    float32 w;

    point4d() : x(0.f), y(0.f), z(0.f), w(1.f) {}
    point4d(float32 x_, float32 y_, float32 z_) : x(x_), y(y_), z(z_), w(1.0f) {}
    point4d(float32 x_, float32 y_, float32 z_, float32 w_) : x(x_), y(y_), z(z_), w(w_) {}
    point4d(float32* m)
    { 
        BAJA_VERIFY(m);
        this->x = m[0];
        this->y = m[1]; 
        this->z = m[2];
        this->w = m[3]; 
    }

    point4d(const point4d& point);
    point4d(const point3d& point, float32 w);

    point4d& operator=(const point2d& that);
    point4d& operator=(const point3d& that);
    point4d& operator=(const point4d& that);
    point4d add(const point4d& that) const;
    point4d subtract(const point4d& that) const;
    void set(float32 x, float32 y, float32 z);
};

struct vector4d
{
    float32 x;
    float32 y;
    float32 z;
    float32 w;
};

// quaternion class. Quaternions are an alternative method for describing rotations. 
// They take up much less space than a matrix (just 4 floats) and allow for smooth interpolation from
// one quaternion to the next (i.e. no gimbal locks)
//
struct quaternion
{
    float32 x;
    float32 y;
    float32 z;
    float32 w;

    quaternion() : x(0.f), y(0.f), z(0.f), w(1.f) {}
    quaternion(float32 x_, float32 y_, float32 z_, float32 w_) : x(x_), y(y_), z(z_), w(w_) {}

    quaternion(const quaternion& src) { *this = src; }
    quaternion(const matrix33& matrix);
    quaternion(const matrix44& matrix);

    quaternion& operator=(const quaternion& that);

    void normalize();

    void slerp(const quaternion& dest, float32 t, quaternion* result) const;

    void fromMatrix(const matrix33& matrix);
    void fromMatrix(const matrix44& matrix);

    static void toEuler(const quaternion& quat, vector3d* eulerAngles);

    static void toMatrix(const quaternion& quat, matrix33* matrix);
    void toMatrix(matrix33* matrix) const;

    static void toMatrix(const quaternion& quat, matrix44* matrix);
    void toMatrix(matrix44* matrix) const;
};

// matrix33 class - provides 3x3 matrix functionality to expose rotation functionality. 
//
// A 3x3 matrix could also include scale information but this is not exposed or supported via this class
// so introducing scale into this matrix will result in unpredictable results. 
//
// Translations are not supported in these 3x3 matrices.
//
// The matrix uses a right-handed system and the 
// right/forward/up vectors are in row-major format. Matrix is indexed as [row][col].
//
//    right vector   [a b c]
//    forward vector [e f g]
//    up vector      [i j k]
//
// More functionality, including scale and translations, exists in the matrix44 class.
struct matrix33
{
    float32 m[3][3];

    matrix33();
    matrix33(const matrix33& src);
    matrix33(const matrix44& src);
    matrix33(const quaternion& quat);

    matrix33&  operator=(const matrix33& that);

    void getInverse(matrix33* result) const;

    // methods that apply this matrix to specific types of data

    void apply(const vector2d& p, vector2d* result) const;
    void apply(const vector3d& p, vector3d* result) const;
    void apply(const matrix33& that, matrix33* result) const;

    // methods which directly set data on the existing matrix (current matrix values ignored or overwritten)

    void copyFrom(const matrix44& src);
    void copyFrom(const matrix33& src);
    void setRotationAxisAngle(const vector3d* axis, float32 angle);
    void setRotation(float32 yaw, float32 pitch, float32 roll);
    void makeIdentity();

    // methods that perform specific operations on the existing matrix

    void invert();
    void normalize();
};

// matrix44 class - provides 4x4 matrix functionality. The matrix uses a right-handed system and the 
// right/forward/up vectors are in row-major format. Matrix is indexed as [row][col].
//
//    right vector   [a b c d]
//    forward vector [e f g h]
//    up vector      [i j k l]
//    last row       [0 0 0 1]
//
// Translation of matrix is (d,h,l). For example, a typical matrix for a translation of (5, -2, 2) would be:
//
//          1  0  0  5
//          0  1  0 -2
//          0  0  1  2
//          0  0  0  1
struct matrix44
{
    float32 m[4][4];

    matrix44();
    matrix44(const matrix44& src);
    matrix44(const matrix33& src);
    matrix44(const quaternion& quat);
    matrix44(float32 scale);
    matrix44(float32 xScale, float32 yScale, float32 zScale);

    matrix44&  operator=(const matrix44& that);

    void getTranslation(point3d* result) const;

    void decompose(
        vector3d* scale,
        vector3d* eulerXYZ,
        point3d* translation
        ) const;

    bool decompose(
        vector3d* outScale,
        float32* outShearXY,
        float32* outShearXZ,
        float32* outShearYZ,
        point4d* outPersp,
        vector3d* outEulerXYZ,
        point3d* outTranslation
        ) const;

    // note: assumes matrix columns are unit length
    void getEulerXYZ(vector3d* result) const;
    void getScale(vector3d* result) const;

    float64 getDeterminant() const;
    void getInverse(matrix44* result, float64* outDeterminant) const;
    void getInverse(matrix44* result) const;
    void getTranspose(matrix44* result) const;

    void set(const vector3d& right, const vector3d& up, const vector3d& forward);
    void setRightVector(const vector3d& right);
    void setUpVector(const vector3d& up);
    void setForwardVector(const vector3d& forward);
    void getRightVector(vector3d* outRight) const;
    void getUpVector(vector3d* outUp) const;
    void getForwardVector(vector3d* outForward) const;
    float32 getHandednessSign() const;

    // methods that apply this matrix to specific types of data

    void apply(const point2d& p, point2d* result) const;
    void apply(const vector2d& p, vector2d* result) const;
    void apply(const point3d& p, point3d* result) const;
    void apply(const vector3d& p, vector3d* result) const;
    void apply(const point4d& p, point4d* result) const;
    void apply(const line3d& p, line3d* result) const;
    void apply(const matrix44& that, matrix44* result) const;
    void applyFully(const matrix44& that, matrix44* result) const;

    // methods which directly set data on the existing matrix (current matrix values ignored or overwritten)

    void copyFrom(const matrix33& src);
    void copyFrom(const matrix44& src);
    void setTranslation(const point3d& translation);
    void setRotationZXY(float32 z, float32 x, float32 y);
    void setRotationAxisAngle(const vector3d* axis, float32 angle);

    void setRotationX(float32 angle);
    void setRotationY(float32 angle);
    void setRotationZ(float32 angle);

    // The order the rotations are applied is: X first, Y second, Z last
    void setRotationXYZ(
        float32 rotX,
        float32 rotY,
        float32 rotZ
        );

    void setScale(float32 scale);
    void setScale(float32 xScale, float32 yScale, float32 zScale);

    void makeIdentity();
    void makeCamera(const point3d& lookFrom, const point3d& lookAt);
    void makeCamera(const point3d& lookFrom, const point3d& lookAt, const matrix44& coordSystem);

    void makeOrthographicProjection(
        float32 left, 
        float32 right, 
        float32 top, 
        float32 bottom, 
        float32 nearZ, 
        float32 farZ
        );

    void makePerspectiveProjection(
        float32 widthAtFocalPoint, 
        float32 heightAtFocalPoint, 
        float32 nearZ, 
        float32 focalPointZ, 
        float32 farZ
        );

    void makePerspectiveProjectionLH(
        float32 widthAtFocalPoint, 
        float32 heightAtFocalPoint, 
        float32 nearZ, 
        float32 focalPointZ, 
        float32 farZ
        );

    void makePerspectiveProjection(
        float32 leftAtNear, 
        float32 rightAtNear, 
        float32 topAtNear, 
        float32 bottomAtNear,
        float32 nearZ,
        float32 farZ
        );

    // methods that perform specific operations on the existing matrix

    void invert();
    void normalize();
    void scale(const float32 scale);
    void scale(float32 xScale, float32 yScale, float32 zScale);
    void translate(const vector3d& translation);
};

}} // namespace baja.math